// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

//
// Tracer.cs
//
// Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Diagnostics;
using System.IO;
using System.Xml;
using Mono.Cecil;

namespace Mono.Linker
{
    /// <summary>
    /// Class which implements IDependencyRecorder and writes the dependencies into an XML file.
    /// </summary>
    public class XmlDependencyRecorder : IDependencyRecorder, IDisposable
    {
        public const string DefaultDependenciesFileName = "linker-dependencies.xml";

        private readonly LinkContext context;
        private XmlWriter? writer;
        private Stream? stream;

        public XmlDependencyRecorder(LinkContext context, string? fileName = null)
        {
            this.context = context;

            XmlWriterSettings settings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "\t"
            };

            fileName ??= DefaultDependenciesFileName;

            if (string.IsNullOrEmpty(Path.GetDirectoryName(fileName)) && !string.IsNullOrEmpty(context.OutputDirectory))
            {
                fileName = Path.Combine(context.OutputDirectory, fileName);
                Directory.CreateDirectory(context.OutputDirectory);
            }

            var depsFile = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
            stream = depsFile;

            writer = XmlWriter.Create(stream, settings);
            writer.WriteStartDocument();
            writer.WriteStartElement("dependencies");
            writer.WriteStartAttribute("version");
            writer.WriteString("1.2");
            writer.WriteEndAttribute();
        }

        public void FinishRecording()
        {
            Debug.Assert(writer != null);

            writer.WriteEndElement();
            writer.WriteEndDocument();
            writer.Flush();
        }

        public void Dispose()
        {
            if (writer == null)
                return;

            writer.Dispose();
            stream?.Dispose();
            writer = null;
            stream = null;
        }

        public void RecordDependency(object target, in DependencyInfo reason, bool marked)
        {
            if (writer == null)
                throw new InvalidOperationException();

            if (reason.Kind == DependencyKind.Unspecified)
                return;

            // For now, just report a dependency from source to target without noting the DependencyKind.
            RecordDependency(reason.Source, target, marked);
        }

        public void RecordDependency(object? source, object target, bool marked)
        {
            if (writer == null)
                throw new InvalidOperationException();

            if (!DependencyRecorderHelper.ShouldRecord(context, source) && !DependencyRecorderHelper.ShouldRecord(context, target))
                return;

            // We use a few hacks to work around MarkStep outputting thousands of edges even
            // with the above ShouldRecord checks. Ideally we would format these into a meaningful format
            // however I don't think that is worth the effort at the moment.

            // Prevent useless logging of attributes like `e="Other:Mono.Cecil.CustomAttribute"`.
            if (source is CustomAttribute || target is CustomAttribute)
                return;

            // Prevent useless logging of interface implementations like `e="InterfaceImpl:Mono.Cecil.InterfaceImplementation"`.
            if (source is InterfaceImplementation || target is InterfaceImplementation)
                return;

            if (source != target)
            {
                writer.WriteStartElement("edge");
                if (marked)
                    writer.WriteAttributeString("mark", "1");
                writer.WriteAttributeString("b", DependencyRecorderHelper.TokenString(context, source));
                writer.WriteAttributeString("e", DependencyRecorderHelper.TokenString(context, target));
                writer.WriteEndElement();
            }
        }
    }
}
